//@version=6
// ============================================================================
//  ICT Unicorn Model, Breaker Block & FVG Zone [LunqFX]
// ----------------------------------------------------------------------------
//  Finds the ICT unicorn: a liquidity sweep, a market structure shift that
//  turns the last opposite candle into a breaker block, and a fair value gap
//  from the same displacement that overlaps that breaker. The overlap is the
//  unicorn zone. The script then follows what price does with it — does it
//  come back into the zone, and from there does it reach the targets or fail
//  through the zone — and keeps the score on this symbol.
//
//  NON-REPAINTING: swings are confirmed pivots; the MSS, the zone, the tap,
//  target hits and failures are decided on closed bars. No request.security.
// ============================================================================
indicator("ICT Unicorn Model, Breaker Block & FVG Zone [LunqFX]", "Unicorn [LunqFX]",
     overlay = true, max_bars_back = 500, max_lines_count = 300, max_labels_count = 300, max_boxes_count = 200)

// ─────────────────────────────────────────────────────────────────────────
//  INPUTS
// ─────────────────────────────────────────────────────────────────────────
gM = "Model"
pivLen   = input.int(5, "Swing strength", minval = 2, maxval = 20, group = gM, tooltip = "Bars on each side that confirm a swing high or low. Higher = bigger, fewer models.")
minSweep = input.float(0.1, "Min. liquidity sweep (× ATR)", minval = 0.0, maxval = 5.0, step = 0.05, group = gM,
     tooltip = "How far the new swing must run beyond the previous one to count as a sweep of its liquidity.")
fvgWait  = input.int(5, "Bars to find the FVG after the MSS", minval = 0, maxval = 30, group = gM,
     tooltip = "The displacement can keep printing gaps for a few bars after the structure shift. A gap that overlaps the breaker within this window still forms a unicorn.")
minZone  = input.float(0.15, "Min. unicorn zone height (× ATR)", minval = 0.0, maxval = 3.0, step = 0.05, group = gM,
     tooltip = "A breaker and a gap that barely touch leave a sliver that price runs straight through. Overlaps thinner than this are not counted as a unicorn.")
maxWait  = input.int(60, "Bars to wait for the zone tap", minval = 5, maxval = 500, group = gM)
maxRun   = input.int(120, "Bars after the tap", minval = 5, maxval = 1000, group = gM, tooltip = "After this many bars without a target or failure the model is closed as expired.")

gD = "Drawing"
showParts = input.bool(true, "Breaker and FVG boxes", group = gD, tooltip = "Draw the breaker block and the fair value gap separately, under the unicorn zone where they overlap.")
showStr   = input.bool(true, "Sweep and MSS", group = gD)
showTgt   = input.bool(true, "Targets and invalidation", group = gD)
keepN     = input.int(3, "Models drawn", minval = 1, maxval = 20, group = gD, tooltip = "Finished models older than this are removed from the chart. The track record keeps all of them.")
lblSize   = input.string("Normal", "Label size", options = ["Small", "Normal", "Large"], group = gD)

gC = "Card"
showCard = input.bool(true, "Show card", group = gC)
cardPos  = input.string("Bottom Left", "Position", options = ["Bottom Left", "Top Left", "Top Right", "Bottom Right"], group = gC,
     tooltip = "Bottom left by default: at the top left the card would cover the chart's symbol and indicator legend.")
cardSize = input.string("Normal", "Text size", options = ["Small", "Normal", "Large"], group = gC)

gA = "Alerts"
alForm = input.bool(true, "Alert: unicorn zone formed", group = gA)
alTap  = input.bool(true, "Alert: zone tapped", group = gA)
alRes  = input.bool(true, "Alert: target hit / zone failed", group = gA)

candOn_ = input.bool(true, "Neon candles", group = "Visuals")
candGlw = input.bool(true, "Glow candles", group = "Visuals", tooltip = "Each body is drawn in four bands that brighten towards the close, so the candle reads in the direction it moved. Off = flat translucent bodies.")

// ─────────────────────────────────────────────────────────────────────────
//  THEME — arctic: ice white / royal blue, the unicorn zone in neon pink
// ─────────────────────────────────────────────────────────────────────────
bgLum   = 0.2126 * color.r(chart.bg_color) + 0.7152 * color.g(chart.bg_color) + 0.0722 * color.b(chart.bg_color)
isLight = bgLum > 128
UP      = isLight ? #334155 : #E6F1FF   // bullish
DN      = isLight ? #2457D6 : #3D7BFF   // bearish
UNI     = isLight ? #C2189B : #FF4FD8   // the unicorn zone
UNIX    = isLight ? #A0127F : #FF9BEA   // its text
BRK     = isLight ? #64748B : #94A3B8   // breaker block
FVGC    = isLight ? #0E7490 : #22D3EE   // fair value gap
GREY    = isLight ? #8A8F9C : #6B7080
TXT     = isLight ? #1A1D24 : #E8EAF0
MUTE    = isLight ? #5E6472 : #8F96A6
PANEL   = isLight ? #FFFFFF : #0A0C12
ROW     = isLight ? #EEF1F6 : #141826
INK     = #0A0C12
NONE    = color.new(color.black, 100)

lSize = lblSize == "Small" ? size.small : lblSize == "Large" ? size.large : size.normal
cSize = cardSize == "Small" ? size.small : cardSize == "Large" ? size.large : size.normal

px(float v) =>
    str.tostring(v, format.mintick)

pct(int a, int b) =>
    b > 0 ? str.tostring(a * 100.0 / b, "0") + "%" : "—"

dirInk(int d) =>
    d == 1 ? INK : color.white   // dark text on ice white, white text on royal blue

// ─────────────────────────────────────────────────────────────────────────
//  SWINGS — alternating confirmed pivots
// ─────────────────────────────────────────────────────────────────────────
atr  = ta.atr(14)
ph   = ta.pivothigh(high, pivLen, pivLen)
pl   = ta.pivotlow(low, pivLen, pivLen)
pBar = bar_index - pivLen

var array<int>   sT = array.new_int()     // 1 high, -1 low
var array<float> sP = array.new_float()
var array<int>   sB = array.new_int()

addSwing(int t, float p, int b) =>
    n = array.size(sT)
    if n > 0 and array.get(sT, n - 1) == t
        if t == 1 ? p > array.get(sP, n - 1) : p < array.get(sP, n - 1)
            array.set(sP, n - 1, p)
            array.set(sB, n - 1, b)
    else
        array.push(sT, t)
        array.push(sP, p)
        array.push(sB, b)
        if array.size(sT) > 20
            array.shift(sT)
            array.shift(sP)
            array.shift(sB)
    0   // explicit result, so the if/else branches above are not used as the return value

if not na(ph) and not na(pl)
    if math.abs(ph - close[pivLen]) > math.abs(close[pivLen] - pl)
        addSwing(1, ph, pBar)
    else
        addSwing(-1, pl, pBar)
else if not na(ph)
    addSwing(1, ph, pBar)
else if not na(pl)
    addSwing(-1, pl, pBar)

// ─────────────────────────────────────────────────────────────────────────
//  STATE
//  stage 0 searching · 2 MSS done, looking for the FVG over the breaker
//  stage 3 unicorn zone armed, waiting for the tap · 4 tapped, targets running
//  dir 1 bullish (sell-side swept, MSS up) · -1 bearish
// ─────────────────────────────────────────────────────────────────────────
var int   stage = 0
var int   dir   = 0
var float swX   = na     // the sweep extreme
var int   swB   = na
var float mssP  = na     // the swing the MSS closed through
var int   mssB  = na
var int   bosB  = na
var float bTop  = na     // breaker block
var float bBot  = na
var int   bBar  = na
var float fTop  = na     // fair value gap
var float fBot  = na
var int   fBar  = na
var float zTop  = na     // unicorn zone = breaker ∩ FVG
var float zBot  = na
var int   zBar  = na
var int   armB  = na
var int   tapB  = na
var float dExt  = na     // displacement extreme
var float t1 = na
var float t2 = na
var float inv = na
var int   hitMax = 0
var int   usedSw = na

var box   zBx  = na
var label zLb  = na
var line  t1Ln = na
var line  t2Ln = na
var line  ivLn = na
var label t1Lb = na
var label t2Lb = na
var array<line>  tmpLn = array.new<line>()
var array<label> tmpLb = array.new<label>()

// record: 0 bullish, 1 bearish
var array<int> rForm = array.new_int(2, 0)
var array<int> rTap  = array.new_int(2, 0)
var array<int> rT1   = array.new_int(2, 0)
var array<int> rT2   = array.new_int(2, 0)
var array<int> rFail = array.new_int(2, 0)
var array<int> rWait = array.new_int(2, 0)

// registry: keep only the last N models on the chart
var int mid = 0
var array<line>  gLn  = array.new<line>()
var array<int>   gLnI = array.new_int()
var array<label> gLb  = array.new<label>()
var array<int>   gLbI = array.new_int()
var array<box>   gBx  = array.new<box>()
var array<int>   gBxI = array.new_int()

bump(array<int> a, int i, int add) =>
    array.set(a, i, array.get(a, i) + add)
regLn(line o) =>
    array.push(gLn, o)
    array.push(gLnI, mid)
regLb(label o) =>
    array.push(gLb, o)
    array.push(gLbI, mid)
regBx(box o) =>
    array.push(gBx, o)
    array.push(gBxI, mid)
prune(int upTo) =>
    while array.size(gLnI) > 0 and array.get(gLnI, 0) <= upTo
        line.delete(array.shift(gLn))
        array.shift(gLnI)
    while array.size(gLbI) > 0 and array.get(gLbI, 0) <= upTo
        label.delete(array.shift(gLb))
        array.shift(gLbI)
    while array.size(gBxI) > 0 and array.get(gBxI, 0) <= upTo
        box.delete(array.shift(gBx))
        array.shift(gBxI)
dropTmp() =>
    for o in tmpLn
        line.delete(o)
    for o in tmpLb
        label.delete(o)
    array.clear(tmpLn)
    array.clear(tmpLb)
keepTmp() =>
    for o in tmpLn
        regLn(o)
    for o in tmpLb
        regLb(o)
    array.clear(tmpLn)
    array.clear(tmpLb)

evForm = false
evTap  = false
evHit  = 0
evFrom = 0
evFail = false
evEnd  = false
born   = false   // the unicorn zone is formed on this bar

// the result goes into a label the model already has — nothing floats loose
closeOut(string txt, color c, color tc, bool done) =>
    if done and not na(t2Lb)
        label.set_text(t2Lb, txt + " · T2 " + px(t2))
    else if not na(zLb)
        label.set_text(zLb, txt + " · ZONE " + px(zBot) + " – " + px(zTop))
        label.set_color(zLb, c)
        label.set_textcolor(zLb, tc)
    box.set_right(zBx, bar_index)
    label.set_x(zLb, bar_index)
    prune(mid - keepN)

// overlap of the breaker with a gap; returns na when they do not overlap
overlapTop(float gT, float gB) =>
    oT = math.min(gT, bTop)
    oB = math.max(gB, bBot)
    oT - oB >= math.max(minZone * atr, syminfo.mintick) ? oT : na

// ─────────────────────────────────────────────────────────────────────────
//  STATE MACHINE — closed bars only
// ─────────────────────────────────────────────────────────────────────────
if barstate.isconfirmed and not na(atr)

    // ── 4 · zone tapped: targets, or a close through the far side of the zone
    if stage == 4
        evFrom := hitMax
        failNow = dir == 1 ? close < swX : close > swX   // the model fails only when the sweep itself gives way
        if failNow
            evFail := true
        else
            for k = 1 to 2
                tk = k == 1 ? t1 : t2
                if hitMax == k - 1 and (dir == 1 ? high >= tk : low <= tk)
                    hitMax := k
                    evHit := k
        if evFail or hitMax == 2 or bar_index - tapB > maxRun
            d = dir == 1 ? 0 : 1
            bump(rT1, d, hitMax >= 1 ? 1 : 0)
            bump(rT2, d, hitMax >= 2 ? 1 : 0)
            bump(rFail, d, evFail ? 1 : 0)
            evEnd := true
            txt = hitMax == 2 ? "UNICORN COMPLETE · " + str.tostring(bar_index - tapB) + " bars" : evFail ? "✕ FAILED · sweep broken" + (hitMax == 1 ? " · T1 reached" : "") : "EXPIRED" + (hitMax == 1 ? " · T1 reached" : "")
            closeOut(txt, color.new(GREY, 0), INK, hitMax == 2)
            stage := 0
            dir := 0

    // ── 3 · zone armed: wait for price to come back into it
    if stage == 3
        dExt := dir == 1 ? math.max(dExt, high) : math.min(dExt, low)
        swept = dir == 1 ? close < swX : close > swX
        tapped = dir == 1 ? low <= zTop : high >= zBot
        if tapped and not swept
            stage := 4
            evTap := true
            tapB := bar_index
            hitMax := 0
            bump(rTap, dir == 1 ? 0 : 1, 1)
            bump(rWait, dir == 1 ? 0 : 1, bar_index - armB)
            t1 := dExt
            t2 := dExt + (dExt - swX) * 0.5   // half the sweep-to-displacement leg beyond the displacement extreme
            mc = dir == 1 ? UP : DN
            regLb(label.new(bar_index, dir == 1 ? low : high, "ZONE TAP", style = dir == 1 ? label.style_label_up : label.style_label_down,
                 color = UNI, textcolor = color.white, size = lSize))
            label.set_text(zLb, "◆ UNICORN " + px(zBot) + " – " + px(zTop) + " · INVALID " + px(swX))
            if showTgt
                t1Ln := line.new(bar_index, t1, bar_index + 1, t1, color = color.new(mc, 25), width = 1, style = line.style_dashed)
                t2Ln := line.new(bar_index, t2, bar_index + 1, t2, color = mc, width = 2)
                t1Lb := label.new(bar_index + 1, t1, "T1 · DISPLACEMENT " + px(t1), style = label.style_label_left, color = color.new(PANEL, 10), textcolor = mc, size = size.small)
                t2Lb := label.new(bar_index + 1, t2, "T2 · EXTENSION " + px(t2), style = label.style_label_left, color = mc, textcolor = dirInk(dir), size = size.small)
                regLn(t1Ln)
                regLn(t2Ln)
                regLb(t1Lb)
                regLb(t2Lb)
        else if swept or bar_index - armB > maxWait
            evEnd := true
            closeOut(swept ? "✕ SWEEP BROKEN · no tap" : "ZONE MISSED", color.new(GREY, 0), INK, false)
            stage := 0
            dir := 0

    // ── 2 · MSS done: the displacement may still print the overlapping gap
    if stage == 2
        dExt := dir == 1 ? math.max(dExt, high) : math.min(dExt, low)
        gT = dir == 1 ? low : low[2]
        gB = dir == 1 ? high[2] : high
        isGap = dir == 1 ? low > high[2] : high < low[2]
        oT = isGap ? overlapTop(gT, gB) : na
        if not na(oT)
            fTop := gT
            fBot := gB
            fBar := bar_index - 1
            zTop := oT
            zBot := math.max(gB, bBot)
            stage := 3
            born := true
        else if bar_index - bosB > fvgWait
            dropTmp()
            stage := 0
            dir := 0

    // ── 0 · searching: swing · opposite swing · a swing that sweeps the first, then a close through the middle one
    if stage == 0 and array.size(sT) >= 3
        n = array.size(sT)
        tA = array.get(sT, n - 3)
        pA = array.get(sP, n - 3)
        pM = array.get(sP, n - 2)
        bM = array.get(sB, n - 2)
        pS = array.get(sP, n - 1)
        bS = array.get(sB, n - 1)
        fresh = na(usedSw) or bS != usedSw
        // bullish: low · high · lower low (sell-side swept) · close above the high
        bull = tA == -1 and pM > pA and pS < pA - minSweep * atr and close > pM
        // bearish: high · low · higher high (buy-side swept) · close below the low
        bear = tA == 1 and pM < pA and pS > pA + minSweep * atr and close < pM
        if fresh and (bull or bear) and bar_index - bM < 400
            usedSw := bS
            dir := bull ? 1 : -1
            swX := pS
            swB := bS
            mssP := pM
            mssB := bM
            bosB := bar_index
            dExt := bull ? high : low
            // breaker: the last opposite-close candle at or just before the middle swing
            bTop := na
            bBot := na
            for i = bar_index - bM to math.min(bar_index - bM + 15, 450)
                if bull ? close[i] > open[i] : close[i] < open[i]
                    bTop := high[i]
                    bBot := low[i]
                    bBar := bar_index - i
                    break
            if na(bTop)
                bTop := high[bar_index - bM]
                bBot := low[bar_index - bM]
                bBar := bM
            // a gap already printed by the displacement, most recent first
            fTop := na
            for k = 0 to math.min(bar_index - bS - 2, 60)
                isG = bull ? low[k] > high[k + 2] : high[k] < low[k + 2]
                if isG
                    gT = bull ? low[k] : low[k + 2]
                    gB = bull ? high[k + 2] : high[k]
                    oT = overlapTop(gT, gB)
                    if not na(oT)
                        fTop := gT
                        fBot := gB
                        fBar := bar_index - k - 1
                        zTop := oT
                        zBot := math.max(gB, bBot)
                        break
            mc = bull ? UP : DN
            array.clear(tmpLn)
            array.clear(tmpLb)
            if showStr
                array.push(tmpLb, label.new(swB, swX, "SWEEP", style = bull ? label.style_label_up : label.style_label_down, color = color.new(PANEL, 0), textcolor = mc, size = size.small))
                array.push(tmpLn, line.new(mssB, mssP, bar_index, mssP, color = mc, width = 2, style = line.style_dashed))
                array.push(tmpLb, label.new(bar_index, mssP, "MSS", style = label.style_label_left, color = NONE, textcolor = mc, size = size.small))
            stage := na(fTop) ? 2 : 3
            born := stage == 3

    // ── the zone is born: draw it and file the model
    if born
        evForm := true
        mid += 1
        armB := bar_index
        bump(rForm, dir == 1 ? 0 : 1, 1)
        keepTmp()
        if showParts
            regBx(box.new(bBar, bTop, bar_index + 1, bBot, border_color = color.new(BRK, 20), border_width = 1, border_style = line.style_dotted,
                 bgcolor = color.new(BRK, 92), text = "BREAKER", text_color = BRK, text_size = size.tiny, text_halign = text.align_left, text_valign = text.align_top))
            regBx(box.new(fBar, fTop, bar_index + 1, fBot, border_color = color.new(FVGC, 30), border_width = 1, border_style = line.style_dashed,
                 bgcolor = color.new(FVGC, 92), text = "FVG", text_color = FVGC, text_size = size.tiny, text_halign = text.align_left, text_valign = text.align_bottom))
        zBar := math.max(bBar, fBar)
        zBx := box.new(zBar, zTop, bar_index + 2, zBot, border_color = UNI, border_width = 2, bgcolor = color.new(UNI, 72))
        zLb := label.new(bar_index + 2, (zTop + zBot) / 2, (dir == 1 ? "▲ " : "▼ ") + "◆ UNICORN " + px(zBot) + " – " + px(zTop), style = label.style_label_left,
             color = UNI, textcolor = color.white, size = lSize)
        regBx(zBx)
        regLb(zLb)

// running drawings follow the current bar
if stage >= 3
    box.set_right(zBx, bar_index + 2)
    label.set_x(zLb, bar_index + 2)
if stage == 4 and showTgt
    line.set_x2(t1Ln, bar_index + 2)
    line.set_x2(t2Ln, bar_index + 2)
    label.set_x(t1Lb, bar_index + 2)
    label.set_x(t2Lb, bar_index + 2)
if evHit > 0 and showTgt
    for k = evFrom + 1 to evHit
        lb = k == 1 ? t1Lb : t2Lb
        label.set_text(lb, "✓ " + label.get_text(lb))
if evEnd and showTgt and not na(t1Ln)
    line.set_x2(t1Ln, bar_index)
    line.set_x2(t2Ln, bar_index)
    label.set_x(t1Lb, bar_index + 1)
    label.set_x(t2Lb, bar_index + 1)
    t1Ln := na
    t2Ln := na
    t1Lb := na
    t2Lb := na

// ─────────────────────────────────────────────────────────────────────────
//  CARD — top-left status card with a segmented progress bar
// ─────────────────────────────────────────────────────────────────────────
cPos = cardPos == "Top Left" ? position.top_left : cardPos == "Bottom Left" ? position.bottom_left : cardPos == "Top Right" ? position.top_right : position.bottom_right
var table card = na

cc(int c, int r, string t, color fg, color bg, string al = text.align_left) =>
    table.cell(card, c, r, t, text_color = fg, bgcolor = bg, text_size = cSize, text_halign = al)

if barstate.islast and showCard
    table.delete(card)
    card := table.new(cPos, 2, 10, bgcolor = PANEL, border_color = PANEL, border_width = 3, frame_color = color.new(UNI, 35), frame_width = 1)
    mc = dir == 1 ? UP : DN
    step = stage == 0 ? 0 : stage == 2 ? 2 : stage == 3 ? 3 : 4 + hitMax
    status = stage == 0 ? "SCANNING" : stage == 2 ? "MSS · LOOKING FOR FVG" : stage == 3 ? "ZONE ARMED" : hitMax == 0 ? "IN ZONE" : "T1 ✓ · RUNNING"
    cc(0, 0, "◆ ICT UNICORN", UNIX, PANEL)
    cc(1, 0, stage == 0 ? status : (dir == 1 ? "▲ " : "▼ ") + status, stage == 0 ? TXT : dirInk(dir), stage == 0 ? ROW : mc, text.align_center)
    bar = ""
    for k = 1 to 6
        bar += k <= step ? "▰" : "▱"
    cc(0, 1, bar + "  " + str.tostring(step) + " / 6", stage == 0 ? MUTE : UNIX, ROW)
    cc(1, 1, "sweep · MSS · FVG · tap · T1 · T2", MUTE, ROW)
    dist = stage == 3 and atr > 0 ? math.abs(close - (dir == 1 ? zTop : zBot)) / atr : na
    cc(0, 2, "Unicorn zone", MUTE, ROW)
    cc(1, 2, stage >= 3 ? px(zBot) + " – " + px(zTop) + (na(dist) ? "" : " · " + str.tostring(dist, "0.0") + " ATR away") : "—", stage >= 3 ? UNIX : MUTE, ROW)
    cc(0, 3, "Breaker · FVG", MUTE, ROW)
    cc(1, 3, stage >= 3 ? px(bBot) + "–" + px(bTop) + " · " + px(fBot) + "–" + px(fTop) : "—", stage >= 3 ? TXT : MUTE, ROW)
    cc(0, 4, "T1 · T2", MUTE, ROW)
    cc(1, 4, stage == 4 ? (hitMax >= 1 ? "✓ " : "") + px(t1) + " · " + (hitMax >= 2 ? "✓ " : "") + px(t2) : "—", stage == 4 ? mc : MUTE, ROW)
    cc(0, 5, "Invalid", MUTE, ROW)
    cc(1, 5, stage >= 3 ? "close beyond the sweep " + px(swX) : "—", stage >= 3 ? TXT : MUTE, ROW)
    nF = array.sum(rForm)
    nTp = array.sum(rTap)
    avgW = nTp > 0 ? array.sum(rWait) * 1.0 / nTp : na
    cc(0, 6, "TRACK RECORD", UNIX, PANEL)
    cc(1, 6, str.tostring(nF) + " unicorns on this chart", MUTE, PANEL)
    cc(0, 7, "Zone tapped", MUTE, PANEL)
    cc(1, 7, pct(nTp, nF) + "   ▲ " + pct(array.get(rTap, 0), array.get(rForm, 0)) + " · ▼ " + pct(array.get(rTap, 1), array.get(rForm, 1)) + " · avg " + (na(avgW) ? "—" : str.tostring(avgW, "0")) + " bars", TXT, PANEL)
    cc(0, 8, "After the tap", MUTE, PANEL)
    cc(1, 8, "T1 " + pct(array.sum(rT1), nTp) + " · T2 " + pct(array.sum(rT2), nTp), TXT, PANEL)
    cc(0, 9, "Failed (sweep broken)", MUTE, PANEL)
    cc(1, 9, pct(array.sum(rFail), nTp) + " of tapped zones", MUTE, PANEL)

// ─────────────────────────────────────────────────────────────────────────
//  ALERTS
// ─────────────────────────────────────────────────────────────────────────
if alForm and evForm
    alert(syminfo.ticker + " " + (dir == 1 ? "bullish" : "bearish") + " unicorn zone " + px(zBot) + " – " + px(zTop), alert.freq_once_per_bar_close)
if alTap and evTap
    alert(syminfo.ticker + " unicorn zone tapped · T1 " + px(t1) + " · T2 " + px(t2) + " · invalid beyond " + px(swX), alert.freq_once_per_bar_close)
if alRes and evHit > 0
    alert(syminfo.ticker + " unicorn T" + str.tostring(evHit) + " hit", alert.freq_once_per_bar_close)
if alRes and evFail
    alert(syminfo.ticker + " unicorn failed: close beyond the sweep " + px(swX), alert.freq_once_per_bar_close)

alertcondition(evForm, "Unicorn zone formed", "{{ticker}} unicorn zone formed")
alertcondition(evTap, "Unicorn zone tapped", "{{ticker}} unicorn zone tapped")
alertcondition(evHit > 0, "Unicorn target hit", "{{ticker}} unicorn target hit")
alertcondition(evFail, "Unicorn failed", "{{ticker}} unicorn failed (sweep broken)")

// ─────────────────────────────────────────────────────────────────────────
//  GLOW CANDLES (LunqFX house style)
// ─────────────────────────────────────────────────────────────────────────
GLOW_UP_DARK  = #E6F1FF
GLOW_DN_DARK  = #3D7BFF
GLOW_UP_LIGHT = #334155
GLOW_DN_LIGHT = #2457D6

glowUp   = isLight ? GLOW_UP_LIGHT : GLOW_UP_DARK
glowDn   = isLight ? GLOW_DN_LIGHT : GLOW_DN_DARK
glowEdge = close >= open ? glowUp : glowDn
glowOn   = candOn_ and candGlw
GLOW_NONE = color.new(color.black, 100)

plotcandle(candOn_ and not glowOn ? open : na, high, low, close, "Candles", color = color.new(glowEdge, 24), wickcolor = color.new(glowEdge, 18), bordercolor = glowEdge, display = display.pane)

gQ = (close - open) / 4.0
plotcandle(glowOn ? open : na, glowOn ? math.max(open, close) : na, glowOn ? math.min(open, close) : na, glowOn ? close : na, "Glow mask", color = chart.bg_color, wickcolor = GLOW_NONE, bordercolor = chart.bg_color, display = display.pane)
plotcandle(glowOn ? open : na, glowOn ? math.max(open, open + gQ) : na, glowOn ? math.min(open, open + gQ) : na, glowOn ? open + gQ : na, "Glow band 1", color = color.new(glowEdge, 78), wickcolor = GLOW_NONE, bordercolor = GLOW_NONE, display = display.pane)
plotcandle(glowOn ? open + gQ : na, glowOn ? math.max(open + gQ, open + 2 * gQ) : na, glowOn ? math.min(open + gQ, open + 2 * gQ) : na, glowOn ? open + 2 * gQ : na, "Glow band 2", color = color.new(glowEdge, 58), wickcolor = GLOW_NONE, bordercolor = GLOW_NONE, display = display.pane)
plotcandle(glowOn ? open + 2 * gQ : na, glowOn ? math.max(open + 2 * gQ, open + 3 * gQ) : na, glowOn ? math.min(open + 2 * gQ, open + 3 * gQ) : na, glowOn ? open + 3 * gQ : na, "Glow band 3", color = color.new(glowEdge, 36), wickcolor = GLOW_NONE, bordercolor = GLOW_NONE, display = display.pane)
plotcandle(glowOn ? open + 3 * gQ : na, glowOn ? math.max(open + 3 * gQ, close) : na, glowOn ? math.min(open + 3 * gQ, close) : na, glowOn ? close : na, "Glow band 4", color = color.new(glowEdge, 8), wickcolor = GLOW_NONE, bordercolor = GLOW_NONE, display = display.pane)
plotcandle(glowOn ? open : na, glowOn ? high : na, glowOn ? low : na, glowOn ? close : na, "Glow wick", color = GLOW_NONE, wickcolor = color.new(glowEdge, 10), bordercolor = color.new(glowEdge, 30), display = display.pane)
